import type { Fn } from '../utils'
import type { AtomLike, AtomMeta } from './'
import {
  _enqueue,
  createAtom,
  EXTENSIONS,
  isAtom,
  named,
  ReatomError,
  STACK,
} from './'

export interface ActionCall<Params extends any[] = any[], Payload = any> {
  params: Params
  payload: Payload
}

/** Autoclearable array of processed events */
export interface ActionState<
  Params extends any[] = any[],
  Payload = any,
> extends Array<ActionCall<Params, Payload>> {}

/** Logic container with atom features */
export interface Action<
  Params extends any[] = any[],
  Payload = any,
> extends AtomLike<ActionState<Params, Payload>, Params, Payload> {}

export type GenericAction<T extends Fn> = T &
  Action<Parameters<T>, ReturnType<T>>

let actionMiddleware = (next: Fn, ...params: any[]) => {
  let frame = STACK[STACK.length - 1]!

  frame.pubs = [STACK[STACK.length - 2]!]

  _enqueue(() => (frame.state = []), 'cleanup')

  return (frame.state = [...frame.state, { params, payload: next(...params) }])
}

/**
 * Type guard to check if a value is a Reatom action.
 *
 * This function determines whether the given value is an action by checking if
 * it's an atom with non-reactive behavior (actions are non-reactive atoms).
 *
 * @param target - The value to check
 * @returns `true` if the value is a Reatom action, `false` otherwise
 */
export let isAction = (target: unknown): target is Action =>
  isAtom(target) && !target.__reatom.reactive

/**
 * Creates a logic and side effect container.
 *
 * Actions are used to encapsulate complex logic, perform side effects (like API
 * calls), and orchestrate multiple state updates. Unlike atoms, actions are
 * meant to be called with parameters and can return values.
 *
 * Actions also have atom-like features (subscribe, extend) and track their call
 * history.
 *
 * @example
 *   // Create an action that fetches data and updates state
 *   const fetchUserData = action(async (userId: string) => {
 *     const response = await wrap(fetch(`/api/users/${userId}`))
 *     const data = await wrap(response.json())
 *
 *     // Update state atoms with the fetched data
 *     userName(data.name)
 *     userEmail(data.email)
 *
 *     return data // Actions can return values
 *   }, 'fetchUserData')
 *
 *   // Call the action
 *   fetchUserData('user123')
 *
 * @template Params - The parameter types the action accepts
 * @template Payload - The return type of the action
 * @param cb - The function containing the action's logic
 * @param name - Optional name for debugging purposes
 * @returns An action instance that can be called with the specified parameters
 */
export let action: {
  <Params extends any[] = any[], Payload = any>(
    cb: (...params: Params) => Payload,
    name?: string,
  ): Action<Params, Payload>

  // special case for type inference of optional parameters
  <Param, Payload = any>(
    cb: (() => Payload) | ((param?: Param) => Payload),
    name?: string,
  ): Action<[Param?], Payload>
  // TODO support the second optional argument (currently falling to unknown in some cases)
  // <Param1, Param2, Payload>(
  //   cb:
  //     | ((param1: Param1) => Payload)
  //     | ((param1: Param1, param2?: Param2) => Payload),
  //   name?: string,
  // ): Action<[Param1, Param2?], Payload>

  <T extends Fn>(cb: T, name?: string): GenericAction<T>
} = <Params extends any[] = any[], Payload = any>(
  cb: (...params: Params) => Payload,
  name: string = named('action', cb.name),
): Action<Params, Payload> => {
  if (typeof cb !== 'function') {
    throw new ReatomError('function expected')
  }

  let target = createAtom({ initState: [], computed: cb }, name) as Action

  Object.assign(target.__reatom, {
    reactive: false,
    middlewares: [actionMiddleware],
  } satisfies Partial<AtomMeta>)

  return (
    EXTENSIONS.length === 0 ? target : target.extend(...EXTENSIONS)
  ) as Action<Params, Payload>
}
